package com.alex.wechat.mp.util;

import java.io.InputStream;
import java.io.StringReader;

import javax.servlet.http.HttpServletRequest;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.jeecgframework.core.util.ApplicationContextUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import com.alex.wechat.app.account.entity.WxAppAccountEntity;
import com.alex.wechat.app.account.service.WxAppAccountServiceI;
import com.alex.wechat.app.config.WxAppContext;
import com.alex.wechat.component.config.WxComponentContext;
import com.alex.wechat.mp.account.entity.WxAccountEntity;
import com.alex.wechat.mp.account.service.WxAccountServiceI;
import com.alex.wechat.mp.config.WxConsts;
import com.alex.wechat.mp.config.WxContext;

import cn.binarywang.wx.miniapp.api.WxMaConfigEx;
import me.chanjar.weixin.common.util.crypto.SHA1;
import me.chanjar.weixin.common.util.crypto.WxCryptUtil;
import me.chanjar.weixin.component.api.WxCompConfigStorage;
import me.chanjar.weixin.message.bean.WxXmlMessage;
import me.chanjar.weixin.mp.api.WxMpConfigStorage;

/**
 * 由于要支持多账号，所以在解密前必须取得 public id，所以扩展微信加密工具，用以支持多账号。
 * 
 * @author Debenson
 * @since 0.1
 */
public class WxCryptUtilEx {
  private static final Logger logger = LoggerFactory.getLogger(WxCryptUtilEx.class);

  private static final ThreadLocal<DocumentBuilder> builderLocal = new ThreadLocal<DocumentBuilder>() {
    @Override
    protected DocumentBuilder initialValue() {
      try {
        return DocumentBuilderFactory.newInstance().newDocumentBuilder();
      } catch (ParserConfigurationException exc) {
        throw new IllegalArgumentException(exc);
      }
    }
  };

  /**
   * 验证请求数据签名
   * 
   * @param token
   * @param request
   * @return
   */
  public static boolean checkSignature(String token, HttpServletRequest request) {
    // 否则只能一一校验签名，这里要求每一个账户的token是不一样的。
    final String signature = request.getParameter(WxConsts.PARAM_SIGNATURE);
    final String nonce = request.getParameter(WxConsts.PARAM_NONCE);
    final String timestamp = request.getParameter(WxConsts.PARAM_TIMESTAMP);
    return StringUtils.isBlank(signature)
        || SignUtil.checkSignature(token, signature, timestamp, nonce);
  }

  /**
   * 从加密字符串转换公众号发来的消息
   *
   * @param is
   * @param mpConfigStorage
   * @param timestamp
   * @param nonce
   * @param msgSignature
   */
  public static WxXmlMessage fromEncryptedXml(InputStream is, WxMpConfigStorage mpConfigStorage,
      String timestamp, String nonce, String msgSignature) {
    String toUserName = null;
    String encyptContent = null;
    try {
      String encryptedContent = IOUtils.toString(is, "UTF-8");
      logger.info("收到的公众号加密消息: {}", encryptedContent);

      Document document = builderLocal.get()
          .parse(new InputSource(new StringReader(encryptedContent)));
      Element root = document.getDocumentElement();
      toUserName = root.getElementsByTagName("ToUserName").item(0).getTextContent();
      encyptContent = root.getElementsByTagName("Encrypt").item(0).getTextContent();
    } catch (Exception e) {
      throw new RuntimeException(e);
    }

    // 必须设置当前公众号账户才能解密
    WxContext.setOriginalId(toUserName);

    // 解密并构造出消息对象
    String plainText = decrypt(msgSignature, timestamp, nonce, encyptContent,
        mpConfigStorage.getToken(), mpConfigStorage.getAesKey(), mpConfigStorage.getAppId());
    return WxXmlMessage.fromXml(plainText);
  }

  /**
   * 从加密字符串转换收到的小程序发来的消息
   *
   * @param is
   * @param appConfigStorage
   * @param timestamp
   * @param nonce
   * @param msgSignature
   */
  public static WxXmlMessage fromEncryptedXml(InputStream is, WxMaConfigEx appConfigStorage,
      String timestamp, String nonce, String msgSignature) {
    String toUserName = null;
    String encyptContent = null;
    try {
      String encryptedContent = IOUtils.toString(is, "UTF-8");
      logger.info("收到的小程序加密消息: {}", encryptedContent);

      Document document = builderLocal.get()
          .parse(new InputSource(new StringReader(encryptedContent)));
      Element root = document.getDocumentElement();
      toUserName = root.getElementsByTagName("ToUserName").item(0).getTextContent();
      encyptContent = root.getElementsByTagName("Encrypt").item(0).getTextContent();
    } catch (Exception e) {
      throw new RuntimeException(e);
    }

    // 必须设置当前公众号账户才能解密
    WxAppContext.setOriginalId(toUserName);

    // 解密并构造出消息对象
    String plainText = decrypt(msgSignature, timestamp, nonce, encyptContent,
        appConfigStorage.getToken(), appConfigStorage.getAesKey(), appConfigStorage.getAppid());
    return WxXmlMessage.fromXml(plainText);
  }

  /**
   * 从加密字符串转换第三方平台发来的消息
   *
   * @param is
   * @param compConfigStorage
   * @param timestamp
   * @param nonce
   * @param msgSignature
   */
  public static WxXmlMessage fromEncryptedXml(InputStream is, WxCompConfigStorage compConfigStorage,
      String timestamp, String nonce, String msgSignature) {
    String compAppId = null, toUserName = null, encyptContent = null;

    try {
      String encryptedContent = IOUtils.toString(is, "UTF-8");
      logger.info("收到的第三方开放平台加密消息: {}", encryptedContent);

      Document document = builderLocal.get()
          .parse(new InputSource(new StringReader(encryptedContent)));
      Element root = document.getDocumentElement();
      encyptContent = root.getElementsByTagName("Encrypt").item(0).getTextContent();

      // 公众号第三方平台可能会接收到两种类型的消息：
      // 1、用户发送给公众号的消息（由公众号第三方平台代收）。此时，消息XML体中，ToUserName（即接收者）为公众号的原始ID（可通过《接口说明》中的获取授权方信息接口来获得）。
      // 2、微信服务器发送给服务自身的事件推送（如取消授权通知，Ticket推送等）。此时，消息XML体中没有ToUserName字段，而是AppId字段，即公众号服务的AppId。这种系统事件推送通知（现在包括推送component_verify_ticket协议和推送取消授权通知），服务开发者收到后也需进行解密，接收到后只需直接返回字符串“success”。
      NodeList nodes = root.getElementsByTagName("AppId");
      if (nodes.getLength() > 0) {
        // 自身事件，AppId为第三方平台的AppId
        compAppId = nodes.item(0).getTextContent();
      }

      nodes = root.getElementsByTagName("ToUserName");
      if (nodes.getLength() > 0) {
        toUserName = nodes.item(0).getTextContent();
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    }

    // 必须设置当前第三方平台账户才能解密
    if (StringUtils.isNotBlank(compAppId)) {
      WxComponentContext.setAppId(compAppId);
    } else if (StringUtils.isNotBlank(toUserName)) {
      // 如果是转换给公众号或小程序的消息，则需要判断是当前账户是小程序还是公众号
      setComponentAppIdByToUserName(toUserName);
    }

    // 解密并构造出消息对象
    String plainText = decrypt(msgSignature, timestamp, nonce, encyptContent,
        compConfigStorage.getToken(), compConfigStorage.getAesKey(), compConfigStorage.getAppId());
    return WxXmlMessage.fromXml(plainText);
  }

  /**
   * 转换成加密的xml格式
   * 
   * @param plainXml
   * @param wxMpConfigStorage
   * @return
   */
  public static String toEncryptedXml(String plainXml, WxMpConfigStorage wxMpConfigStorage) {
    return encrypt(plainXml, wxMpConfigStorage.getToken(), wxMpConfigStorage.getAesKey(),
        wxMpConfigStorage.getAppId());
  }

  /**
   * 转换成加密的xml格式
   * 
   * @param plainXml
   * @param wxAppConfigStorage
   * @return
   */
  public static String toEncryptedXml(String plainXml, WxMaConfigEx wxAppConfigStorage) {
    return encrypt(plainXml, wxAppConfigStorage.getToken(), wxAppConfigStorage.getAesKey(),
        wxAppConfigStorage.getAppid());
  }

  /**
   * 转换成加密的xml格式
   * 
   * @param plainXml
   * @param wxComponentConfigStorage
   * @return
   */
  public static String toEncryptedXml(String plainXml,
      WxCompConfigStorage wxComponentConfigStorage) {
    return encrypt(plainXml, wxComponentConfigStorage.getToken(),
        wxComponentConfigStorage.getAesKey(), wxComponentConfigStorage.getAppId());
  }

  /**
   * 检验消息的真实性，并且获取解密后的明文.
   * <ol>
   * <li>利用收到的密文生成安全签名，进行签名验证</li>
   * <li>若验证通过，则提取xml中的加密消息</li>
   * <li>对消息进行解密</li>
   * </ol>
   *
   * @param msgSignature
   *          签名串，对应URL参数的msg_signature
   * @param timeStamp
   *          时间戳，对应URL参数的timestamp
   * @param nonce
   *          随机串，对应URL参数的nonce
   * @param cipherText
   *          密文，对应Encrypt数据
   * @param token
   * @param aesKey
   * @param appId
   * @return 解密后的原文
   */
  public static String decrypt(String msgSignature, String timeStamp, String nonce,
      String cipherText, String token, String aesKey, String appId) {
    // 验证安全签名
    String signature = SHA1.gen(token, timeStamp, nonce, cipherText);
    if (!signature.equals(msgSignature)) {
      throw new RuntimeException("加密消息签名校验失败");
    }

    // 解密
    WxCryptUtil cryptUtil = new WxCryptUtil(token, aesKey, appId);
    String result = cryptUtil.decrypt(cipherText);
    return result;
  }

  /**
   * 转换成加密的xml格式
   * 
   * @param plainXml
   * @param token
   * @param aesKey
   * @param appId
   * @return
   */
  public static String encrypt(String plainXml, String token, String aesKey, String appId) {
    WxCryptUtil pc = new WxCryptUtil(token, aesKey, appId);
    return pc.encrypt(plainXml);
  }

  /**
   * 通过小程序或公众号的原始id来设置开放平台账户的appId
   * 
   * @param toUserName
   * @return
   */
  private static void setComponentAppIdByToUserName(String toUserName) {
    WxAppAccountServiceI appService = ApplicationContextUtil.getBean(WxAppAccountServiceI.class);
    WxAppAccountEntity appAccount = appService.getAccountByOriginalId(toUserName);
    if (appAccount != null) {
      WxAppContext.setOriginalId(toUserName);
      WxComponentContext.setAppId(appAccount.getComponentAppId());
      return;
    }

    WxAccountServiceI mpService = ApplicationContextUtil.getBean(WxAccountServiceI.class);
    WxAccountEntity mpAccount = mpService.getAccountByOriginalId(toUserName);
    if (mpAccount != null) {
      WxContext.setOriginalId(toUserName);
      WxComponentContext.setAppId(mpAccount.getAppid());
      return;
    }
  }

}
